// ts 重写
import * as io from './socket.io';
const defaultMaxListeners = 10;
const isArray = Array.isArray;
const HEADER = 5;

export class EventEmitter {
    public _events: any
    public _maxListeners: number
    public domain: any;

    constructor() {
        this._events = {};
    }

    emit(...args) {
        var type = arguments[0];
        if (type === 'error') {
            if (!this._events || !this._events.error ||
                (isArray(this._events.error) && !this._events.error.length)) {
                if (this.domain) {
                    var er = arguments[1];
                    er.domain_emitter = this;
                    er.domain = this.domain;
                    er.domain_thrown = false;
                    this.domain.emit('error', er);
                    return false;
                }

                if (arguments[1] instanceof Error) {
                    throw arguments[1]; // Unhandled 'error' event
                } else {
                    throw new Error("Uncaught, unspecified 'error' event.");
                }
                return false;
            }
        }

        if (!this._events) return false;
        var handler = this._events[type];
        if (!handler) return false;

        if (typeof handler == 'function') {
            if (this.domain) {
                this.domain.enter();
            }
            switch (arguments.length) {
                // fast cases
                case 1:
                    handler.call(this);
                    break;
                case 2:
                    handler.call(this, arguments[1]);
                    break;
                case 3:
                    handler.call(this, arguments[1], arguments[2]);
                    break;
                // slower
                default:
                    var l = arguments.length;
                    var args = new Array(l - 1);
                    for (var i = 1; i < l; i++) args[i - 1] = arguments[i];
                    handler.apply(this, args);
            }
            if (this.domain) {
                this.domain.exit();
            }
            return true;

        } else if (isArray(handler)) {
            if (this.domain) {
                this.domain.enter();
            }
            var l = arguments.length;
            var args = new Array(l - 1);
            for (var i = 1; i < l; i++) args[i - 1] = arguments[i];

            var listeners = handler.slice();
            for (var i = 0, l = listeners.length; i < l; i++) {
                listeners[i].apply(this, args);
            }
            if (this.domain) {
                this.domain.exit();
            }
            return true;

        } else {
            return false;
        }
    }

    addListener(type, listener) {
        if ('function' !== typeof listener) {
            throw new Error('addListener only takes instances of Function');
        }

        if (!this._events) this._events = {};

        // To avoid recursion in the case that type == "newListeners"! Before
        // adding it to the listeners, first emit "newListeners".
        this.emit('newListener', type, typeof listener.listener === 'function' ?
            listener.listener : listener);

        if (!this._events[type]) {
            // Optimize the case of one listener. Don't need the extra array object.
            this._events[type] = listener;
        } else if (isArray(this._events[type])) {

            // If we've already got an array, just append.
            this._events[type].push(listener);

        } else {
            // Adding the second element, need to change to array.
            this._events[type] = [this._events[type], listener];

        }

        // Check for listener leak
        if (isArray(this._events[type]) && !this._events[type].warned) {
            var m;
            if (this._maxListeners !== undefined) {
                m = this._maxListeners;
            } else {
                m = defaultMaxListeners;
            }

            if (m && m > 0 && this._events[type].length > m) {
                this._events[type].warned = true;
                console.error('(node) warning: possible EventEmitter memory ' +
                    'leak detected. %d listeners added. ' +
                    'Use emitter.setMaxListeners() to increase limit.',
                    this._events[type].length);
                console.trace();
            }
        }

        return this;
    }
    removeListener(type, listener) {
        if ('function' !== typeof listener) {
            throw new Error('removeListener only takes instances of Function');
        }

        // does not use listeners(), so no side effect of creating _events[type]
        if (!this._events || !this._events[type]) return this;

        var list = this._events[type];

        if (isArray(list)) {
            var position = -1;
            for (var i = 0, length = list.length; i < length; i++) {
                if (list[i] === listener ||
                    (list[i].listener && list[i].listener === listener)) {
                    position = i;
                    break;
                }
            }

            if (position < 0) return this;
            list.splice(position, 1);
        } else if (list === listener ||
            (list.listener && list.listener === listener)) {
            delete this._events[type];
        }

        return this;
    }

    removeAllListeners(type) {
        if (arguments.length === 0) {
            this._events = {};
            return this;
        }

        var events = this._events && this._events[type];
        if (!events) return this;

        if (isArray(events)) {
            events.splice(0);
        } else {
            this._events[type] = null;
        }

        return this;
    }

    listeners(type) {
        if (!this._events) this._events = {};
        if (!this._events[type]) this._events[type] = [];
        if (!isArray(this._events[type])) {
            this._events[type] = [this._events[type]];
        }
        return this._events[type];
    }

    on(type, listener) {
        this.addListener(type, listener)
    }

    setMaxListeners(n: number) {
        this._maxListeners = n;
    }
}

class Message {
    public id: any;
    public route: any;
    public body: any;
    constructor(id, route, body) {
        this.id = id;
        this.route = route;
        this.body = body;
    }
}

export class Pomelo extends EventEmitter {
    public socket = null;
    public id = 1;
    public callbacks: Object
    public params: any
    public debug: boolean
    public io: any;
    public areaId: any

    constructor(io) {
        super();
        this.io = io;
        this.debug = true;
        this.callbacks = {};
    }
    init(params, cb) {
        this.params = params;
        params.debug = true;
        var host = params.host;
        var port = params.port;

        var url = 'ws://' + host;
        if (port) {
            url += ':' + port;
        }

        var socket = this.socket = this.io(url, { 'force new connection': true, reconnect: false });

        socket.on('connect', () => {
            console.log('[pomeloclient.init] websocket connected!');
            if (cb) {
                cb(socket);
            }
        });

        socket.on('reconnect', () => {
            console.log('reconnect');
        });

        socket.on('message', (data) => {
            if (typeof data === 'string') {
                data = JSON.parse(data);
            }
            if (data instanceof Array) {
                this.processMessageBatch(data);
            } else {
                this.processMessage(data);
            }
        });

        socket.on('error', (err) => {
            console.log(err);
        });

        socket.on('disconnect', (reason) => {
            this.emit('disconnect', reason);
        });
    }

    disconnect() {
        if (this.socket) {
            this.socket.disconnect();
            this.socket = null;
        }
    }

    request(route, ...args) {
        if (!route) {
            return;
        }
        var msg = {};
        var cb;
        var argument = Array.prototype.slice.apply(arguments);
        if (argument.length === 2) {
            if (typeof argument[1] === 'function') {
                cb = argument[1];
            } else if (typeof argument[1] === 'object') {
                msg = argument[1];
            }
        } else if (argument.length === 3) {
            msg = argument[1];
            cb = argument[2];
        }
        msg = this.filter(msg, route);
        this.id++;
        this.callbacks[this.id] = cb;
        var sg = this.encode(this.id, route, msg);
        this.socket.send(sg);
    }

    notify(route, msg) {
        this.request(route, msg);
    };

    processMessage(msg: any) {
        if (msg.id) {
            //if have a id then find the callback function with the request
            var cb = this.callbacks[msg.id];

            delete this.callbacks[msg.id];
            if (typeof cb !== 'function') {
                console.log('[pomeloclient.processMessage] cb is not a function for request ' + msg.id);
                return;
            }

            cb(msg.body);
            return;
        }

        // server push message or old format message
        processCall(msg);

        //if no id then it should be a server push message
        function processCall(msg) {
            var route = msg.route;
            if (!!route) {
                if (!!msg.body) {
                    var body = msg.body.body;
                    if (!body) { body = msg.body; }
                    this.emit(route, body);
                } else {
                    this.emit(route, msg);
                }
            } else {
                this.emit(msg.body.route, msg.body);
            }
        }
    }

    processMessageBatch(msgs) {
        for (var i = 0, l = msgs.length; i < l; i++) {
            this.processMessage(msgs[i]);
        }
    }

    encode(id, route, msg) {
        var msgStr = JSON.stringify(msg);
        if (route.length > 255) { throw new Error('route maxlength is overflow'); }
        var byteArray = new Uint16Array(HEADER + route.length + msgStr.length);
        var index = 0;
        byteArray[index++] = (id >> 24) & 0xFF;
        byteArray[index++] = (id >> 16) & 0xFF;
        byteArray[index++] = (id >> 8) & 0xFF;
        byteArray[index++] = id & 0xFF;
        byteArray[index++] = route.length & 0xFF;
        for (var i = 0; i < route.length; i++) {
            byteArray[index++] = route.charCodeAt(i);
        }
        for (var i = 0; i < msgStr.length; i++) {
            byteArray[index++] = msgStr.charCodeAt(i);
        }
        return bt2Str(byteArray, 0, byteArray.length);
    }

    decode(msg: any) {
        var idx, len = msg.length, arr = new Array(len);
        for (idx = 0; idx < len; ++idx) {
            arr[idx] = msg.charCodeAt(idx);
        }
        var index = 0;
        var buf = new Uint16Array(arr);
        var id = ((buf[index++] << 24) | (buf[index++]) << 16 | (buf[index++]) << 8 | buf[index++]) >>> 0;
        var routeLen = buf[HEADER - 1];
        var route = bt2Str(buf, HEADER, routeLen + HEADER);
        var body = bt2Str(buf, routeLen + HEADER, buf.length);
        return new Message(id, route, body);
    }

    filter(msg, route) {
        if (route.indexOf('area.') === 0) {
            msg.areaId = this.areaId;
        }

        // msg.timestamp = Date.now();
        return msg;
    }
}

var bt2Str = function (byteArray, start, end) {
    var result = "";
    for (var i = start; i < byteArray.length && i < end; i++) {
        result = result + String.fromCharCode(byteArray[i]);
    };
    return result;
}

const pomelo = new Pomelo(io);
export default pomelo
